﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.IO;

namespace Ivan.Dm.ChromeRemoteDebugger.Transport {
    public enum ConnectionTerminationReason { 
        ConnectionFailed, 
        ConnectionInterrupted, 
        HandshakeFailed 
    }

    public struct ChromeHandshake {
        public const string Request = "ChromeDevToolsHandshake\r\n";
        public const string Response = "ChromeDevToolsHandshake";
    }

    public class Connection {
        private const int BufferSize = 65536;
        private const int TransportDelayMilliseconds = 200;
        private const int ManualTerminationDelayMilliseconds = 1000;
        private static Connection Instance { get; set; }
        private Socket Socket { get; set; }
        private Queue<Message> OutgoingQueue { get; set; }
        private Queue<Message> IncomingQueue { get; set; }
        private object OutgoingQueueLock { get; set; }
        private object IncomingQueueLock { get; set; }
        private ManualResetEvent TerminationEvent { get; set; }
        private IConnectionStateListener StateListener { get; set; }
        private IMessageListener MessageListener { get; set; }
        private Message PingMessage { get; set; }
        private NetworkStream Stream { get; set; }
        private StreamReader Reader { get; set; }
        private StreamWriter Writer { get; set; }

        private Connection() {
            OutgoingQueue = new Queue<Message>();
            IncomingQueue = new Queue<Message>();
            OutgoingQueueLock = new object();
            IncomingQueueLock = new object();
            TerminationEvent = new ManualResetEvent(false);
        }

        public static Connection Current {
            get {
                if (Connection.Instance == null)
                    Connection.Instance = new Connection();
                return Connection.Instance;
            }
        }

        private void FireConnectionInterrupted() {
            TerminationEvent.Set();
            StateListener.ConnectionTerminated(ConnectionTerminationReason.ConnectionInterrupted);
            CloseStream();
        }

        private void IncomingQueueDispatcherThread() {
            while (true) {
                if (TerminationEvent.WaitOne(Connection.TransportDelayMilliseconds))
                    return;
                lock (IncomingQueueLock) {
                    if (IncomingQueue.Count == 0)
                        continue;
                    MessageListener.MessageReceived(IncomingQueue.Dequeue());
                }
            }
        }

        private void ReaderThread() {
            Message message = null;
            var response = new byte[Connection.BufferSize];
            while (true) {
                if (TerminationEvent.WaitOne(Connection.TransportDelayMilliseconds))
                    return;
                try {
                    message = Message.ReadFromStream(Reader);
                }
                catch (IOException) {
                    if (TerminationEvent.WaitOne(0))
                        return;
                    FireConnectionInterrupted();
                    return;
                }
                if (message == null) {
                    MessageListener.MailformedMessageReceived();
                    continue;
                }
                lock (IncomingQueueLock) {
                    IncomingQueue.Enqueue(message);
                }
            }
        }

        private void WriterThread() {
            Message message = null;
            while (true) {
                if (TerminationEvent.WaitOne(Connection.TransportDelayMilliseconds))
                    return;
                lock (OutgoingQueueLock) {
                    message = OutgoingQueue.Count == 0 ? PingMessage : OutgoingQueue.Dequeue();
                }
                try {
                    message.WriteToStream(Writer);
                }
                catch (IOException) {
                    if (TerminationEvent.WaitOne(0))
                        return;
                    FireConnectionInterrupted();
                    return;
                }
            }
        }

        private bool Handshake() {
            try {
                Socket.Send(ASCIIEncoding.ASCII.GetBytes(ChromeHandshake.Request));
                var response = new byte[Connection.BufferSize];
                int bytesReceived = Socket.Receive(response);
                string strResponse = ASCIIEncoding.ASCII.GetString(response);
                if (strResponse.Substring(0, bytesReceived) != ChromeHandshake.Response) {
                    Terminate();
                    StateListener.ConnectionTerminated(ConnectionTerminationReason.HandshakeFailed);
                    return false;
                }
            }
            catch (SocketException) {
                Terminate();
                StateListener.ConnectionTerminated(ConnectionTerminationReason.ConnectionInterrupted);
                return false;
            }
            return true;
        }

        private void PrepareConnection() {
            Terminate();
            IncomingQueue.Clear();
            OutgoingQueue.Clear();
            TerminationEvent.Reset();
            Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            Socket.ReceiveBufferSize = Connection.BufferSize;
            Socket.SendBufferSize = Connection.BufferSize;
        }

        private void StartThreads() {
            var writerThread = new Thread(this.WriterThread);
            writerThread.Start();
            var readerThread = new Thread(this.ReaderThread);
            readerThread.Start();
            var incomingQueueDispatcherThread = new Thread(this.IncomingQueueDispatcherThread);
            incomingQueueDispatcherThread.Start();
        }

        private void InitializeStream() {
            Stream = new NetworkStream(Socket);
            Reader = new StreamReader(Stream);
            Writer = new StreamWriter(Stream);
        }

        private void CloseStream() {
            if (Stream == null)
                return;
            Reader.Dispose();
            Writer.Dispose();
            Stream.Dispose();
        }

        public void Establish(IPEndPoint endPoint,
                              IConnectionStateListener stateListener,
                              IMessageListener messageListener,
                              Message pingMessage) {
            StateListener = stateListener;
            MessageListener = messageListener;
            PingMessage = pingMessage;
            PrepareConnection();
            try {
                Socket.Connect(endPoint);
            }
            catch (SocketException) {
                StateListener.ConnectionTerminated(ConnectionTerminationReason.ConnectionFailed);
                return;
            }
            if (!Handshake())
                return;
            InitializeStream();
            StartThreads();
            StateListener.ConnectionEstablished();
        }

        public void SendMessage(Message message) {
            lock (OutgoingQueueLock) {
                OutgoingQueue.Enqueue(message);
            }
        }

        public void Terminate() {
            OutgoingQueue.Clear();
            if (Socket == null)
                return;
            try {
                TerminationEvent.Set();
                Thread.Sleep(Connection.ManualTerminationDelayMilliseconds);
                CloseStream();
                Socket.Close();
            }
            catch (SocketException) {
                //TODO
            }
        }
    }
}
